/**
    kcookiespolicies.cpp - Cookies configuration

    Original Authors
    SPDX-FileCopyrightText: Waldo Bastian <bastian@kde.org>
    SPDX-FileCopyrightText: 1999 David Faure <faure@kde.org>
    SPDX-FileCopyrightText: 2008 Urs Wolfer <uwolfer @ kde.org>

    Re-written by:
    SPDX-FileCopyrightText: 2000 Dawit Alemayehu <adawit@kde.org>

    SPDX-License-Identifier: GPL-2.0-or-later
*/

// Own
#include "kcookiespolicies.h"

// Local
#include "ksaveioconfig.h"

// Qt
#include <QCheckBox>
#include <QPushButton>
#include <QDBusReply>
#include <QDBusInterface>

// KDE
#include <KIconLoader>
#include <KMessageBox>
#include <KConfigGroup>
#include <KLocalizedString>
#include <KConfig>
#include <QUrl>


// QUrl::fromAce/toAce don't accept a domain that starts with a '.', like we do here.
// So we use these wrappers.
QString tolerantFromAce (const QByteArray& _domain)
{
    QByteArray domain (_domain);
    const bool hasDot = domain.startsWith ('.');
    if (hasDot)
        domain.remove (0, 1);
    QString ret = QUrl::fromAce(domain);
    if (hasDot) {
        ret.prepend(QLatin1Char('.'));
    }
    return ret;
}

static QByteArray tolerantToAce (const QString& _domain)
{
    QString domain (_domain);
    const bool hasDot = domain.startsWith(QLatin1Char('.'));
    if (hasDot)
        domain.remove (0, 1);
    QByteArray ret = QUrl::toAce (domain);
    if (hasDot) {
        ret.prepend ('.');
    }
    return ret;
}


KCookiesPolicies::KCookiesPolicies (QWidget* parent)
    : KCModule (parent), mSelectedItemsCount(0)
{
    mUi.setupUi (this);
    mUi.kListViewSearchLine->setTreeWidget (mUi.policyTreeWidget);
    QList<int> columns;
    columns.append (0);
    mUi.kListViewSearchLine->setSearchColumns (columns);

    mUi.pbNew->setIcon (QIcon::fromTheme(QStringLiteral("list-add")));
    mUi.pbChange->setIcon (QIcon::fromTheme(QStringLiteral("edit-rename")));
    mUi.pbDelete->setIcon (QIcon::fromTheme(QStringLiteral("list-remove")));
    mUi.pbDeleteAll->setIcon (QIcon::fromTheme(QStringLiteral("edit-delete")));

    // Connect the main swicth :) Enable/disable cookie support
    connect (mUi.cbEnableCookies, &QAbstractButton::toggled,
             this, &KCookiesPolicies::cookiesEnabled);
    connect (mUi.cbEnableCookies, &QAbstractButton::toggled,
             this, &KCookiesPolicies::configChanged);

    // Connect the preference check boxes...
    connect (mUi.cbRejectCrossDomainCookies, &QAbstractButton::toggled,
             this, &KCookiesPolicies::configChanged);
    connect (mUi.cbAutoAcceptSessionCookies, &QAbstractButton::toggled,
             this, &KCookiesPolicies::configChanged);

    connect (mUi.rbPolicyAsk, &QAbstractButton::toggled,
             this, &KCookiesPolicies::configChanged);
    connect (mUi.rbPolicyAccept, &QAbstractButton::toggled,
             this, &KCookiesPolicies::configChanged);
    connect (mUi.rbPolicyAcceptForSession, &QAbstractButton::toggled,
              this, &KCookiesPolicies::configChanged);
    connect (mUi.rbPolicyReject, &QAbstractButton::toggled,
             this, &KCookiesPolicies::configChanged);
    // Connect signals from the domain specific policy listview.
    connect (mUi.policyTreeWidget, &QTreeWidget::itemSelectionChanged,
             this, &KCookiesPolicies::selectionChanged);
    connect (mUi.policyTreeWidget, &QTreeWidget::itemDoubleClicked,
             this, QOverload<>::of(&KCookiesPolicies::changePressed));

    // Connect the buttons...
    connect(mUi.pbNew, &QAbstractButton::clicked, this, QOverload<>::of(&KCookiesPolicies::addPressed));
    connect(mUi.pbChange, &QAbstractButton::clicked, this, QOverload<>::of(&KCookiesPolicies::changePressed));
    connect(mUi.pbDelete, &QAbstractButton::clicked, this, &KCookiesPolicies::deletePressed);
    connect(mUi.pbDeleteAll, &QAbstractButton::clicked, this, &KCookiesPolicies::deleteAllPressed);
}

KCookiesPolicies::~KCookiesPolicies()
{
}

void KCookiesPolicies::configChanged ()
{
    //kDebug() << "KCookiesPolicies::configChanged...";
    Q_EMIT changed (true);
}

void KCookiesPolicies::cookiesEnabled (bool enable)
{
    mUi.bgDefault->setEnabled (enable);
    mUi.bgPreferences->setEnabled (enable);
    mUi.gbDomainSpecific->setEnabled (enable);
}

void KCookiesPolicies::setPolicy (const QString& domain)
{
    QTreeWidgetItemIterator it (mUi.policyTreeWidget);
    bool hasExistingPolicy = false;
    while (*it) {
        if ((*it)->text(0) == domain) {
            hasExistingPolicy = true;
            break;
        }
        ++it;
    }

    if (hasExistingPolicy) {
        changePressed((*it), false);
    } else {
        addPressed(domain);
    }
}

void KCookiesPolicies::changePressed()
{
    changePressed(mUi.policyTreeWidget->currentItem());
}

void KCookiesPolicies::addPressed()
{
    addPressed(QString());
}

void KCookiesPolicies::changePressed(QTreeWidgetItem* item, bool state)
{
    Q_ASSERT(item);
    const QString oldDomain(item->text (0));

    KCookiesPolicySelectionDlg pdlg (this);
    pdlg.setWindowTitle (i18nc ("@title:window", "Change Cookie Policy"));
    pdlg.setPolicy (KCookieAdvice::strToAdvice(QString::fromLatin1(mDomainPolicyMap.value(oldDomain))));
    pdlg.setEnableHostEdit (state, oldDomain);

    if (pdlg.exec() && !pdlg.domain().isEmpty()) {
        const QString newDomain = tolerantFromAce (pdlg.domain().toLatin1());
        int advice = pdlg.advice();
        if (newDomain == oldDomain || !handleDuplicate (newDomain, advice)) {
            mDomainPolicyMap[newDomain] = KCookieAdvice::adviceToStr(advice);
            item->setText(0, newDomain);
            item->setText(1, i18n (mDomainPolicyMap.value(newDomain)));
            configChanged();
        }
    }
}

void KCookiesPolicies::addPressed(const QString& domain, bool state)
{
    KCookiesPolicySelectionDlg pdlg (this);
    pdlg.setWindowTitle (i18nc ("@title:window", "New Cookie Policy"));
    pdlg.setEnableHostEdit(state, domain);

    if (mUi.rbPolicyAccept->isChecked())
        pdlg.setPolicy (KCookieAdvice::Reject);
    else
        pdlg.setPolicy (KCookieAdvice::Accept);

    if (pdlg.exec() && !pdlg.domain().isEmpty()) {
        const QString domain = tolerantFromAce (pdlg.domain().toLatin1());
        int advice = pdlg.advice();

        if (!handleDuplicate (domain, advice)) {
            const char* strAdvice = KCookieAdvice::adviceToStr (advice);
            const QStringList items {
                domain,
                i18n(strAdvice),
            };
            QTreeWidgetItem* item = new QTreeWidgetItem(mUi.policyTreeWidget, items);
            mDomainPolicyMap.insert (item->text(0), strAdvice);
            configChanged();
            updateButtons();
        }
    }
}

bool KCookiesPolicies::handleDuplicate (const QString& domain, int advice)
{
    QTreeWidgetItem* item = mUi.policyTreeWidget->topLevelItem (0);
    while (item != nullptr) {
        if (item->text (0) == domain) {
            const int res = KMessageBox::warningContinueCancel (this,
                            i18n ("<qt>A policy already exists for"
                                  "<center><b>%1</b></center>"
                                  "Do you want to replace it?</qt>", domain),
                            i18nc ("@title:window", "Duplicate Policy"),
                            KGuiItem (i18n ("Replace")));
            if (res == KMessageBox::Continue) {
                mDomainPolicyMap[domain] = KCookieAdvice::adviceToStr(advice);
                item->setText (0, domain);
                item->setText (1, i18n (mDomainPolicyMap.value(domain)));
                configChanged();
                return true;
            } else
                return true;  // User Cancelled!!
        }
        item = mUi.policyTreeWidget->itemBelow (item);
    }
    return false;
}

void KCookiesPolicies::deletePressed()
{
    QTreeWidgetItem* nextItem = nullptr;

    const QList<QTreeWidgetItem *> selectedItems = mUi.policyTreeWidget->selectedItems();
    for (const QTreeWidgetItem *item : selectedItems) {
        nextItem = mUi.policyTreeWidget->itemBelow (item);
        if (!nextItem)
            nextItem = mUi.policyTreeWidget->itemAbove (item);

        mDomainPolicyMap.remove (item->text(0));
        delete item;
    }

    if (nextItem)
        nextItem->setSelected (true);

    updateButtons();
    configChanged();
}

void KCookiesPolicies::deleteAllPressed()
{
    mDomainPolicyMap.clear();
    mUi.policyTreeWidget->clear();
    updateButtons();
    configChanged();
}

void KCookiesPolicies::updateButtons()
{
    bool hasItems = mUi.policyTreeWidget->topLevelItemCount() > 0;

    mUi.pbChange->setEnabled((hasItems && mSelectedItemsCount == 1));
    mUi.pbDelete->setEnabled((hasItems && mSelectedItemsCount > 0));
    mUi.pbDeleteAll->setEnabled(hasItems);
}

void KCookiesPolicies::updateDomainList (const QStringList& domainConfig)
{
    mUi.policyTreeWidget->clear();

    QStringList::ConstIterator it = domainConfig.begin();
    for (; it != domainConfig.end(); ++it) {
        QString domain;
        KCookieAdvice::Value advice = KCookieAdvice::Dunno;
        splitDomainAdvice (*it, domain, advice);
        if (!domain.isEmpty()) {
            const QStringList items {
                tolerantFromAce(domain.toLatin1()),
                i18n(KCookieAdvice::adviceToStr(advice)),
            };
            QTreeWidgetItem* item = new QTreeWidgetItem (mUi.policyTreeWidget, items);
            mDomainPolicyMap[item->text(0)] = KCookieAdvice::adviceToStr(advice);
        }
    }    

    mUi.policyTreeWidget->sortItems(0, Qt::AscendingOrder);
}

void KCookiesPolicies::selectionChanged ()
{
    mSelectedItemsCount = mUi.policyTreeWidget->selectedItems().count();
    updateButtons ();
}

void KCookiesPolicies::load()
{
    mSelectedItemsCount = 0;

    KConfig cfg (QStringLiteral("kcookiejarrc"));
    KConfigGroup group = cfg.group ("Cookie Policy");

    bool enableCookies = group.readEntry ("Cookies", true);
    mUi.cbEnableCookies->setChecked (enableCookies);
    cookiesEnabled (enableCookies);

    // Warning: the default values are duplicated in kcookiejar.cpp
    KCookieAdvice::Value advice = KCookieAdvice::strToAdvice (group.readEntry (
                                      "CookieGlobalAdvice", "Accept"));
    switch (advice) {
    case KCookieAdvice::Accept:
        mUi.rbPolicyAccept->setChecked (true);
        break;
    case KCookieAdvice::AcceptForSession:
        mUi.rbPolicyAcceptForSession->setChecked (true);
        break;
    case KCookieAdvice::Reject:
        mUi.rbPolicyReject->setChecked (true);
        break;
    case KCookieAdvice::Ask:
    case KCookieAdvice::Dunno:
    default:
        mUi.rbPolicyAsk->setChecked (true);
    }

    bool enable = group.readEntry ("RejectCrossDomainCookies", true);
    mUi.cbRejectCrossDomainCookies->setChecked (enable);

    bool sessionCookies = group.readEntry ("AcceptSessionCookies", true);
    mUi.cbAutoAcceptSessionCookies->setChecked (sessionCookies);
    updateDomainList (group.readEntry ("CookieDomainAdvice", QStringList()));

    if (enableCookies) {
        updateButtons();
    }
}

void KCookiesPolicies::save()
{
    KConfig cfg (QStringLiteral("kcookiejarrc"));
    KConfigGroup group = cfg.group ("Cookie Policy");

    bool state = mUi.cbEnableCookies->isChecked();
    group.writeEntry ("Cookies", state);
    state = mUi.cbRejectCrossDomainCookies->isChecked();
    group.writeEntry ("RejectCrossDomainCookies", state);
    state = mUi.cbAutoAcceptSessionCookies->isChecked();
    group.writeEntry ("AcceptSessionCookies", state);

    QString advice;
    if (mUi.rbPolicyAccept->isChecked()) {
        advice = QString::fromLatin1(KCookieAdvice::adviceToStr(KCookieAdvice::Accept));
    } else if (mUi.rbPolicyAcceptForSession->isChecked()) {
         advice = QString::fromLatin1(KCookieAdvice::adviceToStr(KCookieAdvice::AcceptForSession));
    } else if (mUi.rbPolicyReject->isChecked()) {
        advice = QString::fromLatin1(KCookieAdvice::adviceToStr(KCookieAdvice::Reject));
    } else {
        advice = QString::fromLatin1(KCookieAdvice::adviceToStr(KCookieAdvice::Ask));
    }

    group.writeEntry ("CookieGlobalAdvice", advice);

    QStringList domainConfig;
    QMapIterator<QString, const char*> it (mDomainPolicyMap);
    while (it.hasNext()) {
        it.next();
        const QString policy = QLatin1String(tolerantToAce(it.key())) + QLatin1Char(':') + QLatin1String(it.value());
        domainConfig << policy;
    }

    group.writeEntry ("CookieDomainAdvice", domainConfig);
    group.sync();

    // Update the cookiejar...
    if (!mUi.cbEnableCookies->isChecked()) {
        QDBusInterface kded (QStringLiteral("org.kde.kcookiejar5"), QStringLiteral("/modules/kcookiejar"), QStringLiteral("org.kde.KCookieServer"), QDBusConnection::sessionBus());
        kded.call (QStringLiteral("shutdown"));
    } else {
        QDBusInterface kded (QStringLiteral("org.kde.kcookiejar5"), QStringLiteral("/modules/kcookiejar"), QStringLiteral("org.kde.KCookieServer"), QDBusConnection::sessionBus());
        QDBusReply<void> reply = kded.call (QStringLiteral("reloadPolicy"));
        if (!reply.isValid())
            KMessageBox::sorry (nullptr, i18n ("Unable to communicate with the cookie handler service.\n"
                                         "Any changes you made will not take effect until the service "
                                         "is restarted."));
    }

    // Force running io-slave to reload configurations...
    KSaveIOConfig::updateRunningIOSlaves (this);
    Q_EMIT changed (false);
}


void KCookiesPolicies::defaults()
{
    mUi.cbEnableCookies->setChecked (true);
    mUi.rbPolicyAsk->setChecked (true);
    mUi.rbPolicyAccept->setChecked (false);
    mUi.rbPolicyAcceptForSession->setChecked (false);
    mUi.rbPolicyReject->setChecked (false);
    mUi.cbRejectCrossDomainCookies->setChecked (true);
    mUi.cbAutoAcceptSessionCookies->setChecked (false);
    mUi.policyTreeWidget->clear();
    mDomainPolicyMap.clear();

    cookiesEnabled (mUi.cbEnableCookies->isChecked());
    updateButtons();
}

void KCookiesPolicies::splitDomainAdvice (const QString& cfg, QString& domain,
        KCookieAdvice::Value& advice)
{
    int sepPos = cfg.lastIndexOf(QLatin1Char(':'));

    // Ignore any policy that does not contain a domain...
    if (sepPos <= 0)
        return;

    domain = cfg.left (sepPos);
    advice = KCookieAdvice::strToAdvice (cfg.mid (sepPos + 1));
}

QString KCookiesPolicies::quickHelp() const
{
    return i18n ("<h1>Cookies</h1><p>Cookies contain information that KDE"
                 " application using the HTTP protocol (like Konqueror) stores"
                 " on your computer from a remote Internet server. This means"
                 " that a web server can store information about you and your"
                 " browsing activities on your machine for later use. You might"
                 " consider this an invasion of privacy.</p><p>However, cookies are"
                 " useful in certain situations. For example, they are often used"
                 " by Internet shops, so you can 'put things into a shopping"
                 " basket'. Some sites require you have a browser that supports"
                 " cookies.</p><p>Because most people want a compromise between privacy"
                 " and the benefits cookies offer, KDE offers you the ability to"
                 " customize the way it handles cookies. You might, for example"
                 " want to set KDE's default policy to ask you whenever a server"
                 " wants to set a cookie or simply reject or accept everything."
                 " For example, you might choose to accept all cookies from your"
                 " favorite shopping web site. For this all you have to do is"
                 " either browse to that particular site and when you are presented"
                 " with the cookie dialog box, click on <i> This domain </i> under"
                 " the 'apply to' tab and choose accept or simply specify the name"
                 " of the site in the <i> Domain Specific Policy </i> tab and set"
                 " it to accept. This enables you to receive cookies from trusted"
                 " web sites without being asked every time KDE receives a cookie.</p>"
                );
}

